﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Audio;

namespace TestGame.GameLib {
    enum Orientation {
        Top, Left, Bottom, Right
    }

    /// <summary>
    /// Represents two Blobs that are fused together as the user controls them.
    /// Once they collide with something in the array, other blobs or the array 
    /// bounds, a timer ticks down. Once the timer is finished, the two Blobs'
    /// postions are finalized and they are added to the array.
    /// </summary>
    class BlobCombo {
        #region Fields

        /// <summary>
        /// The Lead blob, the blob that's position is rarely changed when the
        /// orientation of the BlobCombo changes.
        /// </summary>
        Blob lead;

        /// <summary>
        /// The projected position of the Lead Blob. Projected position is the
        /// position that based off the current lead Blob's position, where it
        /// will be eventually placed in the array.
        /// </summary>
        Vector2 leadProjected;

        /// <summary>
        /// The Follow blob, the blob that's position is always changed when the
        /// orientation of the BlobCombo changes.
        /// </summary>
        Blob follow;

        /// <summary>
        /// The projected position of the follow Blob. Projected position is
        /// the position that based off the current follow Blob's position,
        /// where it will be eventually placed in the array.
        /// </summary>
        Vector2 followProjected;

        /// <summary>
        /// The orientation of the BlobCombo. When the user wishes to rotate 
        /// the BlobCombo, the orientation changes along with the two Blob's
        /// positions.
        /// </summary>
        Orientation orientation;

        /// <summary>
        /// The timer used to keep track of how much longer the BlobCombo has
        /// until it is added to the array.
        /// </summary>
        TimeSpan collisionTimer;

        /// <summary>
        /// A flag used to keep track if the BlobCombo is currently colliding 
        /// with anything. While the algorithm behavior doesn't call for this
        /// flag, it is used to play the sound effect only once, rather than 
        /// 60 times a second.
        /// </summary>
        bool isColliding;

        /// <summary>
        /// The current speed to be used by the BlobCombo during the Update
        /// method.
        /// </summary>
        float currentSpeed;

        /// <summary>
        /// The speed to be used if Down key is not held down. 
        /// </summary>
        const float RegularSpeed = 1.0f;

        /// <summary>
        /// The faster speed to be used if Down key is held down.
        /// </summary>
        const float FastSpeed = 4.0f;
        
        /// <summary>
        /// SoundEffect to be used when a BlobCombo is added to the array.
        /// </summary>
        static SoundEffect blobAddSfx;

        /// <summary>
        /// SoundEffect to be used when a BlobCombo responds to user input.
        /// </summary>
        static SoundEffect blobMoveSfx;

        /// <summary>
        /// The texture used by the projection drawing.
        /// </summary>
        static Texture2D projectionTexture;

        #endregion

        #region Properties

        /// <summary>
        /// Gets the Blob represented by being the Lead
        /// </summary>
        public Blob LeadBlob {
            get { return lead; }
        }

        /// <summary>
        /// Gets the Blob represented by being the Follow
        /// </summary>
        public Blob FollowBlob {
            get { return follow; }
        }

        #endregion

        #region Initialization

        /// <summary>
        /// Constructs a BlobCombo using the given random number generator.
        /// </summary>
        /// <param name="rng"></param>
        public BlobCombo(Random rng) : this((byte)rng.Next(5), (byte)rng.Next(5)) { }

        /// <summary>
        /// Constructs a BlobCombo using the given BlobTypes
        /// </summary>
        /// <param name="leadType"></param>
        /// <param name="followType"></param>
        public BlobCombo(byte leadType, byte followType) {
            int x = PlayingArea.INITIAL_BLOB_X;
            int y = PlayingArea.INITIAL_BLOB_Y;

            lead = new Blob(leadType, new Vector2(x, y));
            follow = new Blob(followType, new Vector2(x, y + 1));

            /* Set the projected values to something large, so they won't be
             * improperly displayed during the first draw() call. */
            leadProjected = new Vector2(100, 100);
            followProjected = new Vector2(100, 100);

            orientation = Orientation.Bottom;
            currentSpeed = RegularSpeed;
            isColliding = false;
            collisionTimer = TimeSpan.FromSeconds(1.0);
        }

        /// <summary>
        /// Loads any textures and sound effects needed by the BlobCombo.
        /// </summary>
        /// <param name="content"></param>
        public static void LoadContent(ContentManager content) {
            blobAddSfx = content.Load<SoundEffect>("blip-sound");
            blobMoveSfx = content.Load<SoundEffect>("blip-move2");
            projectionTexture = content.Load<Texture2D>("ballGrey");
        }

        #endregion

        #region Game Methods
        
        /// <summary>
        /// Draws the BlobCombo and their projected positions in the array.
        /// </summary>
        /// <param name="spriteBatch"></param>
        /// <param name="content"></param>
        public void Draw(SpriteBatch spriteBatch, ContentManager content) {
            float difference = (Blob.TileSize - projectionTexture.Height) / 2.0f;
            Vector2 textureOffset = new Vector2(difference, difference);
            Vector2 leadDrawPos = Blob.CalculateDrawPosition(leadProjected);
            Vector2 followDrawPos = Blob.CalculateDrawPosition(followProjected);

            // Draw the projections first, so the appear behind the Blobs themselves.
            spriteBatch.Draw(projectionTexture, textureOffset + leadDrawPos, lead.FindColor());
            spriteBatch.Draw(projectionTexture, textureOffset + followDrawPos, follow.FindColor());

            // Draw the two Blobs
            lead.Draw(spriteBatch, content);
            follow.Draw(spriteBatch, content);
        }

        /// <summary>
        /// Updates the BlobCombo position and projection values. Requires the 
        /// BlobArray to test for collisions.
        /// </summary>
        /// <param name="gameTime"></param>
        /// <param name="array"></param>
        public void Update(GameTime gameTime, BlobArray array) {
            if (BottomCellOccupied(lead, array) || BottomCellOccupied(follow, array)) {
                collisionTimer = collisionTimer.Subtract(gameTime.ElapsedGameTime);
                
                /* Round the two positions when they collide with something, so
                 * they will not overlap other blobs. */
                lead.RoundPosition();
                follow.RoundPosition();

                if (!isColliding) {
                    blobAddSfx.Play();
                }

                isColliding = true;
            }
            else {
                collisionTimer = TimeSpan.FromSeconds(1.0);
                lead.Position.Y += (0.04f * currentSpeed);
                follow.Position.Y += (0.04f * currentSpeed);
                isColliding = false;
            }

            UpdateProjectedPositions(array);
        }

        /// <summary>
        /// Determines if the timer has run out of time, and the BlobCombo is
        /// thus, finished.
        /// </summary>
        /// <returns></returns>
        public Boolean IsFinished() {
            return collisionTimer <= TimeSpan.Zero;
        }

        /// <summary>
        /// Handles any input for the BlobCombo. Requires the current BlobArray
        /// to test for any collisions.
        /// </summary>
        /// <param name="input"></param>
        /// <param name="array"></param>
        public void HandleInput(InputState input, BlobArray array) {
            // Rotation Checks
            if (input.IsRotateClockwiseButtonPressed()) {
                RotateClockwise(array);
            }
            else if (input.IsRotateCounterClockwiseButtonPressed()) {
                RotateCounterClockwise(array);
            }

            // Speed Checks
            if (input.IsKeyPressed(Keys.Down)) {
                currentSpeed = FastSpeed;
            }
            else {
                currentSpeed = RegularSpeed;
            }

            // Shift Checks
            if (input.IsLeftButtonPressed()) {
                MoveLeft(array);
            }
            else if (input.IsRightButtonPressed()) {
                MoveRight(array);
            }

            // Hard Drop Check
            if (input.IsHardDownPressed()) {
                HardDrop(array);
            }
        }

        #endregion

        #region Projection Method

        /// <summary>
        /// The method in-charge of updating the projected positions for the 
        /// BlobCombo. 
        /// </summary>
        /// <param name="array"></param>
        void UpdateProjectedPositions(BlobArray array) {
            leadProjected.X = lead.X;
            leadProjected.Y = lead.Y;

            followProjected.X = follow.X;
            followProjected.Y = follow.Y;

            while (!BottomCellOccupied(leadProjected, array)) {
                leadProjected.Y++;
            }

            while (!BottomCellOccupied(followProjected, array)) {
                followProjected.Y++;
            }

            /* These two if statements fix a bug that occurs when the two Blobs are on top of
             * each other, thus have the same projected position. We use the simple cases of 
             * changing either the follow's or lead's projection if it's on top. */
            if (orientation == Orientation.Top) {
                followProjected.Y--;
            }
            else if (orientation == Orientation.Bottom) {
                leadProjected.Y--;
            }
        }

        #endregion

        #region Grid Searching Methods

        /// <summary>
        /// Determines if the given position has any elements behlod it.
        /// </summary>
        /// <param name="position"></param>
        /// <param name="grid"></param>
        /// <returns></returns>
        bool BottomCellOccupied(Vector2 position, BlobArray grid) {
            int x = (int)position.X;
            int y = (int)position.Y;

            return grid.GetVacancyFor(x, y + 1) != GridResult.UNOCCUPIED;
        }

        bool BottomCellOccupied(Blob current, BlobArray grid) {
            return grid.GetVacancyFor(current.X, current.Y + 1) != GridResult.UNOCCUPIED;
        }

        bool TopCellOccupied(Blob current, BlobArray grid) {
            return grid.GetVacancyFor(current.X, current.Y - 1) != GridResult.UNOCCUPIED;
        }

        bool RightCellOccupied(Blob current, BlobArray grid) {
            int yOffset = (isColliding) ? 0 : 1;

            return grid.GetVacancyFor(current.X + 1, current.Y + yOffset) != GridResult.UNOCCUPIED;
        }

        bool LeftCellOccupied(Blob current, BlobArray grid) {
            int yOffset = (isColliding) ? 0 : 1;

            return grid.GetVacancyFor(current.X - 1, current.Y + yOffset) != GridResult.UNOCCUPIED;
        }

        #endregion

        #region Blob Movement Methods

        /// <summary>
        /// A HardDrop is a very fast drop that makes the BlobCombo fall 
        /// instantly to the projected value.
        /// </summary>
        /// <param name="array"></param>
        void HardDrop(BlobArray array) {
            collisionTimer = TimeSpan.Zero;

            lead.RoundPosition();
            follow.RoundPosition();

            while (!BottomCellOccupied(lead, array) && !BottomCellOccupied(follow, array)) {
                lead.Position.Y++;
                follow.Position.Y++;
            }
        }

        /// <summary>
        /// Attempts to rotate the BlobCombo clockwise. If the rotate was 
        /// successful, the BlobCombo's orientation and position updates.
        /// </summary>
        /// <param name="array"></param>
        void RotateClockwise(BlobArray array) {
            int length = Enum.GetValues(typeof(Orientation)).Length;
            int value = (int)orientation;
            Orientation old = this.orientation;

            orientation = (Orientation)((value + 1) % length);

            if (!UpdateBlobOrientation(array)) {
                orientation = old;
            }
            else {
                blobMoveSfx.Play();
            }
        }

        /// <summary>
        /// Attempts to rotate the BlobCombo counterclockwise. If the rotate 
        /// was successful, the BlobCombo's orientation and position updates.
        /// </summary>
        /// <param name="array"></param>
        void RotateCounterClockwise(BlobArray array) {
            int length = Enum.GetValues(typeof(Orientation)).Length;
            int value = (int)orientation - 1;
            Orientation old = this.orientation;

            orientation = (Orientation)(value < 0 ? length - 1 : value);

            if (!UpdateBlobOrientation(array)) {
                orientation = old;
            }
            else {
                blobMoveSfx.Play();
            }
        }

        /// <summary>
        /// Attempts to shift the BlobCombo to the left. If the shift was 
        /// successful, the BlobCombo's position updates.
        /// </summary>
        /// <param name="array"></param>
        void MoveLeft(BlobArray array) {
            if (!LeftCellOccupied(lead, array) && !LeftCellOccupied(follow, array)) {
                lead.Position.X--;
                follow.Position.X--;

                blobMoveSfx.Play();
            }
        }

        /// <summary>
        /// Attempts to shift the BlobCombo to the right. If the shift was 
        /// successful, the BlobCombo's position updates.
        /// </summary>
        /// <param name="array"></param>
        void MoveRight(BlobArray array) {
            if (!RightCellOccupied(lead, array) && !RightCellOccupied(follow, array)) {
                lead.Position.X++;
                follow.Position.X++;

                blobMoveSfx.Play();
            }
        }

        /// <summary>
        /// Attempts to update the Blob's orientation when it is changed. If 
        /// new orientation would cause overlaps with other Blobs or push the 
        /// BlobCombo out of bounds, the update is unsuccessful. Otherwise, it 
        /// will update the BlobCombo's position and orientation.
        /// </summary>
        /// <param name="array"></param>
        /// <returns></returns>
        bool UpdateBlobOrientation(BlobArray array) {
            bool wasSuccessful = true;

            switch (orientation) {
                case Orientation.Top:
                    if (!TopCellOccupied(lead, array)) {
                        follow.Position.X = lead.Position.X;
                        follow.Position.Y = lead.Position.Y - 1;
                    }
                    else {
                        wasSuccessful = false;
                    }
                    
                    break;
                case Orientation.Left:
                    if (!LeftCellOccupied(lead, array)) {
                        follow.Position.X = lead.Position.X - 1;
                        follow.Position.Y = lead.Position.Y;
                    }
                    else if (!RightCellOccupied(lead, array)) {
                        lead.Position.X = lead.Position.X + 1;
                        follow.Position.X = lead.Position.X - 1;
                        follow.Position.Y = lead.Position.Y;
                    }
                    else {
                        wasSuccessful = false;
                    }
                    
                    break;
                case Orientation.Right:
                    if (!RightCellOccupied(lead, array)) {
                        follow.Position.X = lead.Position.X + 1;
                        follow.Position.Y = lead.Position.Y;
                    }
                    else if (!LeftCellOccupied(lead, array)) {
                        lead.Position.X = lead.Position.X - 1;
                        follow.Position.X = lead.Position.X + 1;
                        follow.Position.Y = lead.Position.Y;
                    }   
                    else {
                        wasSuccessful = false;
                    }
                    
                    break;
                case Orientation.Bottom:
                    if (!BottomCellOccupied(lead.Position + new Vector2(0, 1), array)) {
                        follow.Position.X = lead.Position.X;
                        follow.Position.Y = lead.Position.Y + 1;
                    }
                    else if (!TopCellOccupied(lead, array)) {
                        lead.Position.Y = lead.Position.Y - 1;
                        follow.Position.X = lead.Position.X;
                        follow.Position.Y = lead.Position.Y + 1;
                    }
                    else {
                        wasSuccessful = false;
                    }

                    break;
            }

            return wasSuccessful;
        }

        #endregion
    }
}
